Skip to content

[mypyc] add support for contextlib.contextmanager - #22267

Open
hauntsaninja wants to merge 7 commits into
python:masterfrom
hauntsaninja:context
Open

hauntsaninja wants to merge 7 commits into
python:masterfrom
hauntsaninja:context

Conversation

@hauntsaninja

@hauntsaninja hauntsaninja commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Should make self check like 6-10% faster and torch 8-12% faster (compiled, clean cache, not parallel)

Fixes mypyc/mypyc#905

Co-authored by Codex

@hauntsaninja
hauntsaninja marked this pull request as ready for review October 22, 2026 04:10
@hauntsaninja
hauntsaninja requested a review from JukkaL October 22, 2026 04:42
@ilevkivskyi

Copy link
Copy Markdown
Member

I guess you can say that this fixes mypyc/mypyc#905, right? (Btw I didn't look in details, @JukkaL would be a better reviewer here.)

ctm_def_node = None
# Since we construct the call to the method ourselves in _transform_with_contextmanager,
# we're a little paranoid about doing things in the wrong order in weird cases
if not find_walrus_targets(expr) and not expr_has_suspend(expr):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to add tests for these cases.

Comment on lines +1154 to +1155
and len(dec.decorators) == 1
and isinstance(dec1 := dec.decorators[0], mypy.nodes.RefExpr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add a test where the function has other decorators, i don't think we currently have one like that.

Comment on lines +1288 to +1295
wrapped_call = mypy.nodes.CallExpr(
mypy.nodes.MemberExpr(expr.callee, "__wrapped__"),
expr.args,
expr.arg_kinds,
expr.arg_names,
)
wrapped_call.line = line
gen = builder.accept(wrapped_call)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this results in a generic Python attribute access + generic function call at runtime which we could optimize for compiled functions decorated with @contextmanager. for those we could call the generated C function directly.

assert isinstance(expr.callee, mypy.nodes.MemberExpr)
receiver = builder.accept(receiver_expr)
method = builder.py_get_attr(receiver, expr.callee.name, line)
wrapped = builder.py_get_attr(method, "__wrapped__", line)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for non-final methods we should check and optimize only if __wrapped__ actually exists because it's legal to override a contextmanager method with a regular method in a subclass.

we might be able to optimize the call for final methods and for methods of compiled classes that don't allow interpreted subclasses by calling the C function directly.

Comment on lines +1325 to +1337
def test_no_yield() -> None:
try:
with cm_no_yield():
print("UNREACHABLE")
print("UNREACHABLE")
except RuntimeError as e:
e_any = cast(Any, e)
print(
type(e).__name__,
str(e),
e_any.__cause__ is None,
e_any.__suppress_context__,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general comment about the run tests: when adding new tests we usually check using inline assertions instead of checking the printed output. i would prefer to use inline assertions here as well since it makes the tests easier to read - i don't have to scroll down to see the desired behavior.

Comment on lines +1348 to +1353
try:
with cm_no_stop() as v:
print("entered body", v)
print("UNREACHABLE")
except RuntimeError as e:
print(type(e).__name__, str(e))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general comment: could use assertRaises for checking exceptions.


print("ret result", inner())

[file driver.py]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use inline assertions i think you could also simplify the new tests by not using a custom driver.py. if there's no custom one then the default runs every function in the compiled file prefixed with test_.

Comment on lines +1327 to +1328
with cm_no_yield():
print("UNREACHABLE")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to also add an irbuild test with a specialized with statement since the run tests don't prove that the statement was compiled using the specialization.

@hauntsaninja

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, will address!

I used prints instead of runtime asserts because the exception logic is actually kind of tricky and was a source of bugs for me, very easy to accidentally suppress an exception

I couldn't see an easy way to find the generated C function. Codex had a diff where it did something that created some new statics, it benchmarked only a little bit better, but you're right that it could be a little more robust. Let me investigate it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Specialize contextlib.contextmanager

3 participants